package com.morphiastic;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.elasticsearch.common.xcontent.XContentBuilder;

import com.google.code.morphia.annotations.Id;
import com.google.code.morphia.utils.ReflectionUtils;
import com.morphiastic.analysis.analyzers.ElasticAnalyzer;
import com.morphiastic.analysis.filters.ElasticFilter;
import com.morphiastic.analysis.tokenizers.ElasticTokenizer;
import com.morphiastic.annotations.Analyze;
import com.morphiastic.annotations.ElastiField;
import com.morphiastic.annotations.ElastiFieldEmbedded;
import com.morphiastic.annotations.ElastiFieldType;
import com.morphiastic.annotations.ElastiMap;
import com.morphiastic.conversion.ElasticConverter;
import com.morphiastic.utils.ReflectionTools;
import java.lang.reflect.AccessibleObject;



/**
 * Definition of how an Embedded Entity should be indexed 
 *Represents an Object field in ElasticSearch
 *
 * @author Alex
 *
 */
public class IndexedEmbeddedField implements IndexedField {

//	ElastiFieldEmbedded annotation;
	
	private String fieldOriginalName;
	private String propertyName;
	
	private Class<?> clazz; //the actual type of the field
	
	Integer circularRefMaxDepth;
	
	private List<ElasticAnalyzer> fieldLevelAnalyzers = new ArrayList<ElasticAnalyzer>();
	
	//TODO refactor this a litlle so we can benefit from the abstraction of IndexedField interface
	private LinkedHashMap<Field, StandardIndexedField > standardIndexedFields = new LinkedHashMap<Field, StandardIndexedField>();
	private LinkedHashMap<Field, IndexedEmbeddedField > indexedEmbeddedFields = new LinkedHashMap<Field, IndexedEmbeddedField>();
	private LinkedHashMap<Field, IndexedMapField > indexedMapsFields = new LinkedHashMap<Field, IndexedMapField>();
	
	
	private LinkedHashMap<Field, IndexedField > indexedFields = new LinkedHashMap<Field, IndexedField>();
	
	
	
	private List<ElasticFilter> filterDefs = new ArrayList<ElasticFilter>();
	private List<ElasticTokenizer> tokenizerDefs = new ArrayList<ElasticTokenizer>();
	
	
	/**
	 * Analyzers that were defined in an AnalyzerDefinition annotation inside the class 
	 */
	private List<ElasticAnalyzer> classLevelAnalyzerDefs = new ArrayList<ElasticAnalyzer>();
	
	
	private boolean methodMode = false;
	
	//TODO manage special case of arrays
	
	private ElasticStore es;
	
	
	/**
	 * Creates an IndexedEmbedded field
	 * 
	 * @param field
	 */
	public IndexedEmbeddedField(Field field, ElasticStore es){
		this(field,null,null,es);
	}
	
	/**
	 * Creates an IndexedEmbedded field accessed through a method (think getters)
	 * 
	 * @param field
	 */
	public IndexedEmbeddedField(Method method, ElasticStore es){
		this(method,null,null,es);
	}
	
	/**
	 * constructor with known parameter and "ROOT" maxCircularDepth
	 * 
	 * this constructor is used when we are recursing / navigating inside the object Graph af embedded objects
	 * 
	 * 
	 * 
	 * @param field
	 * @param known
	 * @param maxCircularDepth
	 */
	private IndexedEmbeddedField(Field field, HashMap<AccessibleObject, Integer> known, Integer maxCircularDepth, ElasticStore es){
		this(field.getType(), field.getName(), field.getAnnotation(ElastiFieldEmbedded.class), known, maxCircularDepth, false,es);
	}
	
	
	private IndexedEmbeddedField(Method field, HashMap<AccessibleObject, Integer> known, Integer maxCircularDepth, ElasticStore es){
		this(field.getReturnType(), field.getName(), field.getAnnotation(ElastiFieldEmbedded.class), known, maxCircularDepth, true,es);
	}
	
	
	
	protected IndexedEmbeddedField(Class<?> fieldType, String fieldOriginalName, 
			ElastiFieldEmbedded ann , HashMap<AccessibleObject, Integer> known, 
			Integer maxCircularDepth, boolean methodMode, ElasticStore es){
		
		if(ann==null)throw new IllegalArgumentException("The field was not annotated with the propper ElastiFieldEmbedded annotation");
		this.es = es;
		 this.circularRefMaxDepth = ann.circularRefMaxDepth();
		 this.fieldOriginalName = fieldOriginalName;
		 this.propertyName = ann.name();
		 if(this.propertyName.isEmpty())this.propertyName = fieldOriginalName;
		 this.methodMode = methodMode;
		 
		 
		 //TODO check if we can customize indexname of field ... ? 
//		 if(Collection.class.isAssignableFrom(fieldType)){
//		 
//			 if (fieldType.getGenericType() instanceof ParameterizedType) {  
//		            ParameterizedType pt = (ParameterizedType) genericType;  
//		            System.out.println("raw type: " + pt.getRawType());  
//		            System.out.println("owner type: " + pt.getOwnerType());  
//		            System.out.println("actual type args:"); 
//		            Class  c = (Class) pt.getActualTypeArguments()[0];
//		            System.out.println(c.getName());
//		            System.out.println(c);
//		            return getElasticTypeName(c, genericType);
//		        }  
//			 
//		 }else if(fieldType.isArray()){
//			 Class<?> dataType = fieldType.getComponentType();
//		 }
		 
		 this.clazz = fieldType;
	
		this.filterDefs.addAll(IndexedClass.findFilterDefs(getClazz()));
		this.tokenizerDefs.addAll(IndexedClass.findTokenizerDefs(getClazz()));
		this.classLevelAnalyzerDefs.addAll(IndexedClass.findAnalyzerDefs(getClazz()));
		prepareFields(known, maxCircularDepth);
		
	}
	
	
	
	
	
	
	
	private void prepareFields(HashMap<AccessibleObject, Integer> known, Integer maxCircularDepth){
		
		if(known == null)known = new HashMap<AccessibleObject, Integer>();
		if(maxCircularDepth == null) maxCircularDepth = circularRefMaxDepth;
		
		
		//iterate over fields
		for (Field field : ReflectionUtils.getDeclaredAndInheritedFields(clazz, true)) {
			field.setAccessible(true);
			if (field.isAnnotationPresent(ElastiField.class)){
				addIndexedField(field);
			}
			if(field.isAnnotationPresent(ElastiFieldEmbedded.class)){
				Integer knownDepth = known.get(field);
				if(knownDepth == null || knownDepth <= maxCircularDepth ){
					if(knownDepth == null)knownDepth = 0;
					knownDepth++;
					ElastiFieldEmbedded fan = field.getAnnotation(ElastiFieldEmbedded.class);
					if(fan!=null){
						known.put(field, knownDepth);
						IndexedEmbeddedField indexationRule = null;
						if(field.getType().isArray()){
							indexationRule = new IndexedEmbeddedField(field.getType().getComponentType(), field.getName(), fan, known, maxCircularDepth, false,es);
						}else if(Collection.class.isAssignableFrom(field.getType())){
							Type genericType = field.getGenericType();
							 if (genericType instanceof ParameterizedType) {  
						            ParameterizedType pt = (ParameterizedType) genericType;  
						            Class<?>  c = (Class<?>) pt.getActualTypeArguments()[0];
						            indexationRule = new IndexedEmbeddedField(c, field.getName(), fan, known, maxCircularDepth, false,es);
						      }  
						}else{
							indexationRule = new IndexedEmbeddedField(field, known,maxCircularDepth,es);
						}
//						new IndexedEmbeddedField(field, known, maxCircularDepth );
						indexedEmbeddedFields.put(field, indexationRule);
						indexedFields.put(field,indexationRule);
					}
				}
			}
			if(field.isAnnotationPresent(ElastiMap.class)){
				Integer knownDepth = known.get(field);
				if(knownDepth == null || knownDepth <= maxCircularDepth ){
					if(knownDepth == null)knownDepth = 0;
					knownDepth++;
					ElastiMap fan = field.getAnnotation(ElastiMap.class);
					if(fan!=null){
						known.put(field, knownDepth);
						IndexedMapField indexationRule = new IndexedMapField( field, known, maxCircularDepth,es);
						indexedMapsFields.put(field, indexationRule);
						indexedFields.put(field,indexationRule);
					}
				}
			}
		}
		
		
		//TODO
		//now iterate over methods 
	}
	

	/**
	 * adds a regular IndexedField
	 * @param field
	 */
	private void addIndexedField(Field field){
		//get each annotation value and create an IndexationRule
		ElastiField fan = field.getAnnotation(ElastiField.class);
		if(fan!=null){
			StandardIndexedField indexationRule = new StandardIndexedField(field,es);
			
			this.standardIndexedFields.put(field, indexationRule);
			this.indexedFields.put(field, indexationRule);
			
			ElasticAnalyzer indexAn = indexationRule.getAnalyzer();
			ElasticAnalyzer searchAn = indexationRule.getSearchAnalyzer();
			this.fieldLevelAnalyzers.add(indexAn);
			if(searchAn!=null)this.fieldLevelAnalyzers.add(searchAn);
		}
	}

	
	 
	
	
	
	
	/**
	 * Get all the analyzers that were declared in the embedded entity
	 * + all the ones that were nested inside deeper level embeded objects
	 * 
	 * 
	 * WARNING there is no check for circular reference and we might get infinite loops 
	 * TODO fix this as soon as possible
	 * @return
	 */
	public Set<ElasticAnalyzer> getAnalyzers(){
		
		Set<ElasticAnalyzer> allAnalyzerDefs = new HashSet<ElasticAnalyzer>();
		allAnalyzerDefs.addAll(classLevelAnalyzerDefs);
		allAnalyzerDefs.addAll(fieldLevelAnalyzers);
		
		for (IndexedEmbeddedField embeddedField : indexedEmbeddedFields.values()) {
			allAnalyzerDefs.addAll(embeddedField.getAnalyzers());
		}
		for (IndexedMapField mapField : indexedMapsFields.values()) {
			allAnalyzerDefs.addAll(mapField.getAnalyzers());
		}
		return allAnalyzerDefs;
	}
	
	
	public Set<ElasticFilter> getFilterDefinitions(){
		Set<ElasticFilter> allFilterDefs = new HashSet<ElasticFilter>();
		allFilterDefs.addAll(filterDefs);
		
		for (IndexedEmbeddedField embeddedField : indexedEmbeddedFields.values()) {
			allFilterDefs.addAll(embeddedField.getFilterDefinitions());
		}
		
		return allFilterDefs;
	}

	
	
	public Set<ElasticTokenizer> getTokenizerDefinitions(){
		Set<ElasticTokenizer> allTokenizerDefs = new HashSet<ElasticTokenizer>();
		allTokenizerDefs.addAll(tokenizerDefs);
		for (IndexedEmbeddedField embeddedField : indexedEmbeddedFields.values()) {
			allTokenizerDefs.addAll(embeddedField.getTokenizerDefinitions());
		}
		return allTokenizerDefs;
	}

		
	/**
	 * 
	 * adds the embeded field mapping to the xContentBuilder
	 * 
	 * 
	 * 
	 * TODO manage circcular dependencies here too as this could go on forever ... 
	 * to manage it properly we could create a maxCircularDepth attribute inside the ElastiFieldEmbedded annotation
	 * and us it in conjunction with a IdentityHashMap that we would pass along each call to addIndexFieldSettings
	 * Dig this ... tough task i think  ... (09.06.12 DONE I think ... )
	 * 
	 * @param mappingBuilder
	 * @throws IOException
	 */
	public void addIndexFieldSettings(XContentBuilder mappingBuilder) throws IOException {
		
//		mappingBuilder.startObject(this.fieldOriginalName);  
		mappingBuilder.startObject(this.propertyName);  
//		mappingBuilder.field("type", "object");
		
		
		mappingBuilder.startObject("properties");
			Set<Field> fields = this.standardIndexedFields.keySet();
				for (Field field : fields) {
					StandardIndexedField rule = this.standardIndexedFields.get(field);
					rule.addIndexFieldSettings(mappingBuilder);
				}
			 
			Set<Field> embeddedFields = this.indexedEmbeddedFields.keySet();
			for(Field field : embeddedFields){
				IndexedEmbeddedField rule = this.indexedEmbeddedFields.get(field);
				rule.addIndexFieldSettings(mappingBuilder);
			}
			
			Set<Field> indexedMapFields = this.indexedMapsFields.keySet();
			for (Field field : indexedMapFields) {
				IndexedMapField rule = this.indexedMapsFields.get(field);
				rule.addIndexFieldSettings(mappingBuilder);
			}
			mappingBuilder.endObject();
		mappingBuilder.endObject();
	}
	
	
	@Override
	public XContentBuilder writeIndexedField(XContentBuilder mappingBuilder ,  Object o, ElasticStore es, Integer maxCircularRefDepth, IdentityHashMap<AccessibleObject, Integer> known) throws IOException {
		
		if(maxCircularRefDepth == null) maxCircularRefDepth = this.circularRefMaxDepth;
		
		//TODO manage case of reference fields
		
		if(o==null){
			String nullStr = null;
			mappingBuilder.field(this.fieldOriginalName, nullStr);
			return mappingBuilder;
		}
		try {
			Field field = ReflectionTools.getDeclaredFieldFromHierarchy(o.getClass(), fieldOriginalName);
			
			//Field  field = o.getClass().getField(this.fieldOriginalName);
			field.setAccessible(true);
			Class<?> fieldType = null;
			Object value = null; 
			boolean shouldIncrementDepth = true;
			//This is dirty but i cant find a way around it as Map.Entry looses the information when dealing with embedded collection ... 
			if(o instanceof Map.Entry){
				Map.Entry mapentry = (Map.Entry)o;
				shouldIncrementDepth = false;
				if(fieldOriginalName.equals("key")){
					System.err.println("key class = " + mapentry.getKey().getClass());
					value = mapentry.getKey();
					fieldType = mapentry.getKey().getClass();
				}else if(fieldOriginalName.equals("value")){
					System.err.println("map value class = " + ((Map.Entry)o).getValue().getClass());
					value = ((Map.Entry)o).getValue();
					fieldType = mapentry.getValue().getClass();
				}
			}else{
				value = field.get(o);
				fieldType = field.getType();
			}
			
			
			
			//check that we are not going too deeply in the circular reference
			Integer curFieldDepth = known.get(field);
			if(curFieldDepth == null)curFieldDepth=0;
			if(curFieldDepth>maxCircularRefDepth){
				return mappingBuilder;
			}
			
			if(value==null){
				String nullStr = null;
				mappingBuilder.field(this.fieldOriginalName, nullStr);
				return mappingBuilder;
			}else{
				if(fieldType.isArray() || Collection.class.isAssignableFrom(fieldType)){
					if(fieldType.isArray()){
						if(shouldIncrementDepth)curFieldDepth++;
						Object[] arr = (Object[]) value;
						mappingBuilder.startArray(this.fieldOriginalName);
							for (Object object : arr) {
								mappingBuilder.startObject();
									for (Map.Entry<Field, IndexedField> indexedFieldEntry: this.indexedFields.entrySet()) {
										indexedFieldEntry.getValue().writeIndexedField(mappingBuilder, object, es, maxCircularRefDepth, known );
									}
								mappingBuilder.endObject();
							}
						mappingBuilder.endArray();
						 
					}else{
						if(shouldIncrementDepth)curFieldDepth++;
						Iterable iterable = (Iterable) value;
						mappingBuilder.startArray(this.fieldOriginalName);
						for (Object object : iterable) {
							mappingBuilder.startObject();
								for (Map.Entry<Field, IndexedField> indexedFieldEntry: this.indexedFields.entrySet()) {
									indexedFieldEntry.getValue().writeIndexedField(mappingBuilder, object, es, maxCircularRefDepth, known );
								}
							mappingBuilder.endObject();
						}
						mappingBuilder.endArray();
					}
				}else{
					if(shouldIncrementDepth)curFieldDepth++;
					 mappingBuilder.startObject(this.fieldOriginalName);
						for (Map.Entry<Field, IndexedField> indexedFieldEntry: this.indexedFields.entrySet()) {
							indexedFieldEntry.getValue().writeIndexedField(mappingBuilder, value, es, maxCircularRefDepth, known );
						}
					mappingBuilder.endObject();
//					mappingBuilder.field(getPropertyName(), converter.toIndexValue(value) );
				}
			}
			
			known.put(field, curFieldDepth);
		} catch (SecurityException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchFieldException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return mappingBuilder;
	}
	
	
	
	
	
	
	
	
	
	public void addFieldSettingsMap(HashMap<String, Object> props){
		
		Map<String,Object> m = new HashMap<String, Object>();
//		m.put("type", "object");  // removed because ES autodetects objects and does not add them to the mapping
		if(!this.fieldOriginalName.equals(this.propertyName))m.put("index_name", this.propertyName);
		HashMap<String,Object> propsMap = new HashMap<String, Object>();
		Set<Field> fields = this.standardIndexedFields.keySet();
		for (Field field : fields) {
			StandardIndexedField rule = this.standardIndexedFields.get(field);
			rule.addFieldSettingsMap(propsMap);
		}
		
		Set<Field> embeddedFields = this.indexedEmbeddedFields.keySet();
		for(Field field : embeddedFields){
			IndexedEmbeddedField rule = this.indexedEmbeddedFields.get(field);
			rule.addFieldSettingsMap(propsMap);
		}
		
		Set<Field> indexedMapFields = this.indexedMapsFields.keySet();
		for (Field field : indexedMapFields) {
			IndexedMapField rule = this.indexedMapsFields.get(field);
			rule.addFieldSettingsMap(propsMap);
		}
		
		
		m.put("properties", propsMap);
			
		props.put(this.fieldOriginalName, m);
			
	}
	
	
	
	
	
	
	
	
	
	
	
	/**
	 * @return the clazz
	 */
	public Class<?> getClazz() {
		return clazz;
	}


	/**
	 * @param clazz the clazz to set
	 */
	public void setClazz(Class<?> clazz) {
		this.clazz = clazz;
	}
	
}
